<?php

/*

    Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.

    This file is part of the Fat-Free Framework (http://fatfreeframework.com).

    This is free software: you can redistribute it and/or modify it under the
    terms of the GNU General Public License as published by the Free Software
    Foundation, either version 3 of the License, or later.

    Fat-Free Framework is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.

*/
declare(strict_types=1);

namespace Shoofly;

//! Authorization/authentication plug-in
class Auth
{
    //@{ Error messages
    public const E_LDAP = 'LDAP connection failure';
    public const E_SMTP = 'SMTP connection failure';
    //@}

    /** @var string|object Auth storage */
    protected $storage;
    /** @var \Shoofly\Database\Cursor Mapper object */
    protected $mapper;
    /** @var array Storage options */
    protected $args = [];
    /** @var callable Custom compare function */
    protected $func;

    /**
    *   Instantiate class
    *   @return object
    *   @param $storage string|object
    *   @param $args array
    *   @param $func callback
    **/
    public function __construct($storage, array $args = null, $func = null)
    {
        if (is_object($storage) && is_a($storage, '\Shoofly\Database\Cursor')) {
            $this->storage = $storage->dbtype();
            $this->mapper = $storage;
            unset($ref);
        } else {
            $this->storage = $storage;
        }
        $this->args = $args;
        $this->func = $func;
    }

    /**
    *   Jig storage handler
    *   @return bool
    *   @param $id string
    *   @param $pw string
    *   @param $realm string
    **/
    protected function jig($id, $pw, $realm)
    {
        $success = (bool)
            call_user_func_array(
                [$this->mapper,'load'],
                [
                    array_merge(
                        [
                            '@' . $this->args['id'] . '==?' .
                            ($this->func ? '' : ' AND @' . $this->args['pw'] . '==?') .
                            (isset($this->args['realm']) ?
                                (' AND @' . $this->args['realm'] . '==?') : ''),
                            $id
                        ],
                        ($this->func ? [] : [$pw]),
                        (isset($this->args['realm']) ? [$realm] : [])
                    )
                ]
            );
        if ($success && $this->func) {
            $success = call_user_func($this->func, $pw, $this->mapper->get($this->args['pw']));
        }
        return $success;
    }

    /**
    *   MongoDB storage handler
    *   @return bool
    *   @param $id string
    *   @param $pw string
    *   @param $realm string
    **/
    protected function mongo($id, $pw, $realm)
    {
        $success = (bool)
            $this->mapper->load(
                [$this->args['id'] => $id] +
                ($this->func ? [] : [$this->args['pw'] => $pw]) +
                (isset($this->args['realm']) ?
                    [$this->args['realm'] => $realm] : [])
            );
        if ($success && $this->func) {
            $success = call_user_func($this->func, $pw, $this->mapper->get($this->args['pw']));
        }
        return $success;
    }

    /**
    *   SQL storage handler
    *   @return bool
    *   @param $id string
    *   @param $pw string
    *   @param $realm string
    **/
    protected function sql($id, $pw, $realm)
    {
        $success = (bool)
            call_user_func_array(
                [$this->mapper,'load'],
                [
                    array_merge(
                        [
                            $this->args['id'] . '=?' .
                            ($this->func ? '' : ' AND ' . $this->args['pw'] . '=?') .
                            (isset($this->args['realm']) ?
                                (' AND ' . $this->args['realm'] . '=?') : ''),
                            $id
                        ],
                        ($this->func ? [] : [$pw]),
                        (isset($this->args['realm']) ? [$realm] : [])
                    )
                ]
            );
        if ($success && $this->func) {
            $success = call_user_func($this->func, $pw, $this->mapper->get($this->args['pw']));
        }
        return $success;
    }

    /**
    *   LDAP storage handler
    *   @return bool
    *   @param $id string
    *   @param $pw string
    **/
    protected function ldap($id, $pw)
    {
        $port = (int)($this->args['port'] ?: 389);
        $filter = $this->args['filter'] = $this->args['filter'] ?: "uid=" . $id;
        $this->args['attr'] = $this->args['attr'] ?: ["uid"];
        array_walk(
            $this->args['attr'],
            function ($attr) use (&$filter, $id) {
                $filter = str_ireplace($attr . "=*", $attr . "=" . $id, $filter);
            }
        );
        $dc = @ldap_connect($this->args['dc'], $port);
        if ($dc &&
            ldap_set_option($dc, LDAP_OPT_PROTOCOL_VERSION, 3) &&
            ldap_set_option($dc, LDAP_OPT_REFERRALS, 0) &&
            ldap_bind($dc, $this->args['rdn'], $this->args['pw']) &&
            ($result = ldap_search(
                $dc,
                $this->args['base_dn'],
                $filter,
                $this->args['attr']
            )) &&
            ldap_count_entries($dc, $result) &&
            ($info = ldap_get_entries($dc, $result)) &&
            $info['count'] == 1 &&
            @ldap_bind($dc, $info[0]['dn'], $pw) &&
            @ldap_close($dc)
        ) {
            return in_array($id, (array_map(function ($value) {
                return $value[0];
            },
                array_intersect_key(
                    $info[0],
                    array_flip($this->args['attr'])
                ))), true);
        }
        user_error(self::E_LDAP, E_USER_ERROR);
    }

    /**
    *   SMTP storage handler
    *   @return bool
    *   @param $id string
    *   @param $pw string
    **/
    protected function smtp($id, $pw)
    {
        $socket = @fsockopen(
            (strtolower($this->args['scheme']) == 'ssl' ?
                'ssl://' : '') . $this->args['host'],
            $this->args['port']
        );
        $dialog = function ($cmd = null) use ($socket) {
            if (!is_null($cmd)) {
                fputs($socket, $cmd . "\r\n");
            }
            $reply = '';
            while (!feof($socket) &&
                ($info = stream_get_meta_data($socket)) &&
                !$info['timed_out'] && $str = fgets($socket, 4096)
            ) {
                $reply .= $str;
                if (preg_match(
                    '/(?:^|\n)\d{3} .+\r\n/s',
                    $reply
                )
                ) {
                    break;
                }
            }
            return $reply;
        };
        if ($socket) {
            stream_set_blocking($socket, true);
            $dialog();
            $fw = Base::instance();
            $dialog('EHLO ' . $fw->HOST);
            if (strtolower($this->args['scheme']) == 'tls') {
                $dialog('STARTTLS');
                stream_socket_enable_crypto(
                    $socket,
                    true,
                    STREAM_CRYPTO_METHOD_TLS_CLIENT
                );
                $dialog('EHLO ' . $fw->HOST);
            }
            // Authenticate
            $dialog('AUTH LOGIN');
            $dialog(base64_encode($id));
            $reply = $dialog(base64_encode($pw));
            $dialog('QUIT');
            fclose($socket);
            return (bool)preg_match('/^235 /', $reply);
        }
        user_error(self::E_SMTP, E_USER_ERROR);
    }

    /**
    *   Login auth mechanism
    *   @return bool
    *   @param $id string
    *   @param $pw string
    *   @param $realm string
    **/
    public function login($id, $pw, $realm = null)
    {
        return $this->{$this->storage}($id, $pw, $realm);
    }

    /**
    *   HTTP basic auth mechanism
    *   @return bool
    *   @param $func callback
    **/
    public function basic($func = null)
    {
        $fw = Base::instance();
        $realm = $fw->REALM;
        $hdr = null;
        if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
            $hdr = $_SERVER['HTTP_AUTHORIZATION'];
        } elseif (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
            $hdr = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
        }
        if (!empty($hdr)) {
            list($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW']) =
                explode(':', base64_decode(substr($hdr, 6)));
        }
        if (isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) &&
            $this->login(
                $_SERVER['PHP_AUTH_USER'],
                $func ?
                    $fw->call($func, $_SERVER['PHP_AUTH_PW']) :
                    $_SERVER['PHP_AUTH_PW'],
                $realm
            )
        ) {
            return true;
        }
        if (PHP_SAPI != 'cli') {
            header('WWW-Authenticate: Basic realm="' . $realm . '"');
        }
        $fw->status(401);
        return false;
    }
}
